class Vector2 {
    constructor(x = 0, y = 0) {
        this.x = x;
        this.y = y;
    }

    get length() {
        return Math.sqrt(this.x * this.x + this.y * this.y);
    }

    set length(len) {
        return this.normalize().scale(len)
    }

    get angle() {
        // computes the angle in radians with respect to the positive x-axis
        const angle = Math.atan2(- this.y, - this.x) + Math.PI;

        return angle;
    }

    distanceTo(v) {
        return Math.sqrt(this.distanceToSquared(v));
    }

    distanceToSquared(v) {
        const dx = this.x - v.x, dy = this.y - v.y;
        return dx * dx + dy * dy;
    }

    set(x, y) {
        this.x = x;
        this.y = y;

        return this;
    }

    setX(x) {
        this.x = x;

        return this;
    }

    setY(y) {
        this.y = y;

        return this;
    }

    clone() {
        return new this.constructor(this.x, this.y);
    }

    copy(v) {
        this.x = v.x;
        this.y = v.y;

        return this;
    }

    equals(v) {
        return ((v.x === this.x) && (v.y === this.y));
    }

    fromArray(array, offset = 0) {
        this.x = array[offset];
        this.y = array[offset + 1];

        return this;
    }

    toArray(array = [], offset = 0) {
        array[offset] = this.x;
        array[offset + 1] = this.y;

        return array;
    }

    applyMatrix3(m) {
        const x = this.x, y = this.y;
        const e = m.elements;

        this.x = e[0] * x + e[3] * y + e[6];
        this.y = e[1] * x + e[4] * y + e[7];

        return this;
    }

    clamp(min, max) {
        // assumes min < max, componentwise
        this.x = Math.max(min.x, Math.min(max.x, this.x));
        this.y = Math.max(min.y, Math.min(max.y, this.y));

        return this;
    }

    add(v) {
        const p = v instanceof Vector2;
        this.x += p ? v.x : v;
        this.y += p ? v.y : v;
        return this;
    }

    sub(v) {
        const p = v instanceof Vector2;
        this.x -= p ? v.x : v;
        this.y -= p ? v.y : v;
        return this;
    }

    mul(v) {
        const p = v instanceof Vector2;
        this.x *= p ? v.x : v;
        this.y *= p ? v.y : v;
        return this;
    }

    scale(x) {
        this.x *= x;
        this.y *= x;
        return this;
    }

    dot(v) {
        return this.x * v.x + this.y * v.y;
    }

    cross(v) {
        return this.x * v.y - this.y * v.x;
    }

    normalize() {
        const len = this.length;
        if (len > 0) {
            this.x /= len;
            this.y /= len;
        }

        return this;
    }

    distance(v) {
        const x = this.x - v.x;
        const y = this.y - v.y;
        return Math.sqrt(x * x + y * y);
    }

    rotateAround(center, angle) {
        const c = Math.cos(angle), s = Math.sin(angle);

        const x = this.x - center.x;
        const y = this.y - center.y;

        this.x = x * c - y * s + center.x;
        this.y = x * s + y * c + center.y;

        return this;
    }

    random() {
        this.x = Math.random();
        this.y = Math.random();

        return this;
    }

    static add(v1, v2) {
        const v = v2 instanceof Vector2;
        return new Vector(
            v1.x + (v ? v2.x : v2),
            v1.y + (v ? v2.y : v2)
        );
    }

    static sub(v1, v2) {
        const v = v2 instanceof Vector2;
        return new Vector(
            v1.x - (v ? v2.x : v2),
            v1.y - (v ? v2.y : v2)
        );
    }

    static mul(v1, v2) {
        const v = v2 instanceof Vector2;
        return new Vector(
            v1.x * (v ? v2.x : v2),
            v1.y * (v ? v2.y : v2)
        );
    }

    static dot(v1, v2) {
        return v1.x * v2.x + v1.y * v2.y;
    }
}

class Object2D {
    constructor() {
        this.name = '';
        this.type = 'Object2D';

        this.parent = null;
        this.children = [];
        this.parent = null
        const position = new Vector2();
        // const rotation = new Euler();
        // const quaternion = new Quaternion();
        const scale = new Vector2(1, 1);

        Object.defineProperties(this, {
            position: {
                configurable: true,
                enumerable: true,
                value: position
            },
            /*
            rotation: {
              configurable: true,
              enumerable: true,
              value: rotation
            },
            quaternion: {
              configurable: true,
              enumerable: true,
              value: quaternion
            },
            */
            scale: {
                configurable: true,
                enumerable: true,
                value: scale
            },
        });
    }

    isObject2D() {
        return true
    }

    add(object) {
        if (arguments.length > 1) {
            for (let i = 0; i < arguments.length; i++) {
                this.add(arguments[i]);
            }
            return this;
        }
        if (object === this) {
            console.error('Object2D.add: object can\'t be added as a child of itself.', object);
            return this;
        }
        if (object && object.isObject2D) {
            if (object.parent !== null) {
                object.parent.remove(object);
            }
            object.parent = this;
            this.children.push(object);
        } else {
            console.error('Object2D.add: object not an instance of Object2D.', object);
        }
        return this;
    }

    remove(object) {
        if (arguments.length > 1) {
            for (let i = 0; i < arguments.length; i++) {
                this.remove(arguments[i]);
            }
            return this;
        }

        const index = this.children.indexOf(object);
        if (index !== - 1) {
            object.parent = null;
            this.children.splice(index, 1);
        }
        return this;
    }

    removeFromParent() {
        const parent = this.parent;
        if (parent !== null) {
            parent.remove(this);
        }

        return this;
    }

    clear() {
        for (let i = 0; i < this.children.length; i++) {
            const object = this.children[i];
            object.parent = null;
        }
        this.children.length = 0;

        return this;
    }

    getObjectById(id) {
        return this.getObjectByProperty('id', id);
    }

    getObjectByName(name) {
        return this.getObjectByProperty('name', name);
    }

    getObjectByProperty(name, value) {
        if (this[name] === value) return this;
        for (let i = 0, l = this.children.length; i < l; i++) {
            const child = this.children[i];
            const object = child.getObjectByProperty(name, value);
            if (object !== undefined) {
                return object;
            }
        }

        return undefined;
    }
}

class Scene extends Object2D {
    constructor() {
        super()
        this.type = 'Scene'
    }
}

class Point extends Object2D {
    constructor() {
        super()
        console.log(this.position)
    }
}

class Render {
    constructor(parameters = {}) {
        this._canvas = parameters.canvas !== undefined ? parameters.canvas : this.createCanvasElement()
        this._context = parameters.context !== undefined ? parameters.context : this._canvas.getContext('2d')


        this._width = this._canvas.width;
        this._height = this._canvas.height;
        this._pixelRatio = 1

        this.domElement = this._canvas;
    }
    createCanvasElement() {
        let canvas = document.createElementNS('http://www.w3.org/1999/xhtml', 'canvas')
        canvas.style.display = 'block'

        return canvas
    }
    setSize(width, height, updateStyle = true) {

        this._width = width;
        this._height = height;

        this._canvas.width = Math.floor(width * this._pixelRatio);
        this._canvas.height = Math.floor(height * this._pixelRatio);

        if (updateStyle !== false) {
            this._canvas.style.width = width + 'px';
            this._canvas.style.height = height + 'px';
        }
    }
    setPixelRatio(value) {
        if (value === undefined) return;

        this._pixelRatio = value;
        this.setSize(this._width, this._height, false);
    }

    setAnimationLoop(callback) {
        this.animation = new Animation()
        this.animation.setAnimationLoop(onAnimationFrame)
        onAnimationFrameCallback = callback
        if (callback === null) {
            this.animation.stop()
        } else {
            this.animation.start()
        }
    }

    render(scene) {

    }
}

let onAnimationFrameCallback = null
function onAnimationFrame(time) {
    if (onAnimationFrameCallback) onAnimationFrameCallback(time);
}

function Animation() {
    let isAnimating = false
    let animationLoop = null
    let requestId = null
    this.start = function () {
        if (isAnimating === true) return;
        if (animationLoop === null) return;
        requestAnimationFrame(onAnimationFrame);
        isAnimating = true;
    }
    this.stop = function () {
        cancelAnimationFrame(requestId);
        isAnimating = false;
    }
    this.setAnimationLoop = function setAnimationLoop(callback) {
        animationLoop = callback;
    }
    function onAnimationFrame(time, frame) {
        animationLoop(time, frame);
        requestId = requestAnimationFrame(onAnimationFrame);
    }
}



